/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Collections.ObjectModel;
using System.Xml;

namespace DaveSexton.DocProject.Sandcastle
{
	public sealed class SourceVersion
	{
		#region Public Properties
		public string Version
		{
			get
			{
				return version ?? string.Empty;
			}
			set
			{
				version = value;
			}
		}

		public SystemPath Source
		{
			get
			{
				return path;
			}
			set
			{
				if (value == SystemPath.Empty)
					throw new ArgumentException(Resources.Errors.EmptySystemPathIsInvalid, "value");

				path = value;
			}
		}

		/// <summary>
		/// Gets or sets the path of the versioned source for UI element binding operations.
		/// </summary>
		public string SourcePath
		{
			get
			{
				return boundPath ?? path.CompleteRelativeTo(basePath);
			}
			set
			{
				path = new SystemPath(basePath, value, true);
				boundPath = value;
			}
		}

		public string SourceKey
		{
			get
			{
				return sourceKey;
			}
		}
		#endregion

		#region Private / Protected
		private readonly string sourceKey, basePath;
		private string version, boundPath;
		private SystemPath path;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="SourceVersion" /> class.
		/// </summary>
		public SourceVersion(string sourceKey, string basePath)
		{
			if (string.IsNullOrEmpty(sourceKey))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "sourceKey");

			this.sourceKey = sourceKey;
			this.basePath = basePath;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="SourceVersion" /> class.
		/// </summary>
		public SourceVersion(string sourceKey, SystemPath path, string version)
		{
			if (string.IsNullOrEmpty(sourceKey))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "sourceKey");

			if (path == SystemPath.Empty)
				throw new ArgumentException(Resources.Errors.EmptySystemPathIsInvalid, "path");

			this.sourceKey = sourceKey;
			this.path = path;
			this.version = version;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="SourceVersion" /> class.
		/// </summary>
		public SourceVersion(string sourceKey, string basePath, SystemPath path, string version)
		{
			if (string.IsNullOrEmpty(sourceKey))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "sourceKey");

			if (path == SystemPath.Empty)
				throw new ArgumentException(Resources.Errors.EmptySystemPathIsInvalid, "path");

			this.sourceKey = sourceKey;
			this.path = path;
			this.version = version;
			this.basePath = basePath;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="SourceVersion" /> class.
		/// </summary>
		public SourceVersion(SourceVersion clone)
		{
			if (clone == null)
				throw new ArgumentNullException("clone");

			this.sourceKey = clone.sourceKey;
			this.path = clone.path;
			this.version = clone.version;
			this.basePath = clone.basePath;
		}
		#endregion

		#region Methods
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase")]
		public static string GenerateKey(string relativeToPath, SystemPath source)
		{
			if (source == SystemPath.Empty)
				throw new ArgumentException(Resources.Errors.EmptySystemPathIsInvalid, "source");

			string path;

			if (!string.IsNullOrEmpty(relativeToPath))
				path = source.RelativeTo(relativeToPath);
			else
				path = source.Path;
			
			path = new Uri(path.ToLowerInvariant(), UriKind.RelativeOrAbsolute).ToString();

			using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
			{
				return new Guid(md5.ComputeHash(Encoding.Unicode.GetBytes(path))).ToString();
			}
		}

		[System.Diagnostics.DebuggerHidden]
		internal static IEnumerable<SourceVersion> LoadVersions(string xmlFile, string versionFilesRelativeToPath, out IDictionary<string, string> sourceVersionNames)
		{
			if (string.IsNullOrEmpty(xmlFile))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "xmlFile");

			if (string.IsNullOrEmpty(versionFilesRelativeToPath))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "versionFilesRelativeToPath");

			sourceVersionNames = new Dictionary<string, string>(16);

			XmlReaderSettings settings = new XmlReaderSettings();
			settings.IgnoreComments = true;
			settings.IgnoreProcessingInstructions = true;
			settings.IgnoreWhitespace = true;

			using (XmlReader reader = XmlReader.Create(xmlFile, settings))
			{
				// NOTE: a list must be used to that the iterator is executed immediately, before
				// the "reader" goes out of scope and is closed.
				List<SourceVersion> list = new List<SourceVersion>(LoadVersions(reader, versionFilesRelativeToPath, sourceVersionNames));
				return list.AsReadOnly();
			}
		}

		private static IEnumerable<SourceVersion> LoadVersions(XmlReader reader, string versionFilesRelativeToPath, IDictionary<string, string> sourceVersionNames)
		{
			if (!reader.IsStartElement("sources"))
				yield break;

			while (reader.Read())
			{
				if (reader.IsStartElement("source") && reader.Depth == 1)
				{
					string key = reader.GetAttribute("key");
					
					if (string.IsNullOrEmpty(key))
						reader.Skip();
					else
					{
						sourceVersionNames.Add(key, reader.GetAttribute("version"));

						if (!reader.IsEmptyElement)
						{
							using (XmlReader versions = reader.ReadSubtree())
							{
								reader.ReadStartElement("source");

								foreach (SourceVersion version in LoadVersions(key, versions, versionFilesRelativeToPath))
									yield return version;
							}
						}
					}
				}
			}
		}

		private static IEnumerable<SourceVersion> LoadVersions(string key, XmlReader reader, string versionFilesRelativeToPath)
		{
			while (reader.Read())
			{
				if (reader.IsStartElement("version") && reader.Depth == 1)
				{
					string version = reader.GetAttribute("version");
					string path = reader.GetAttribute("path");

					if (!string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(version))
						yield return new SourceVersion(key, versionFilesRelativeToPath, new SystemPath(versionFilesRelativeToPath, path, true), version);

					if (!reader.IsEmptyElement)
						reader.Skip();
				}
			}
		}

		public static void SaveVersions(string xmlFile, string versionFilesRelativeToPath, IEnumerable<SourceVersion> versions, IDictionary<string, string> sourceVersionNames)
		{
			if (string.IsNullOrEmpty(xmlFile))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "xmlFile");

			if (versions == null)
				throw new ArgumentNullException("versions");

			if (sourceVersionNames == null)
				throw new ArgumentNullException("sourceVersionNames");

			using (FileStream stream = new FileStream(xmlFile, FileMode.OpenOrCreate, FileAccess.Write))
			{
				SerializeAsXml(stream, versionFilesRelativeToPath, versions, sourceVersionNames);
			}
		}

		public static void SerializeAsXml(Stream stream, string versionFilesRelativeToPath, IEnumerable<SourceVersion> versions, IDictionary<string, string> sourceVersionNames)
		{
			if (stream == null)
				throw new ArgumentNullException("stream");

			if (string.IsNullOrEmpty(versionFilesRelativeToPath))
				throw new ArgumentException(Resources.Errors.NullOrEmpty, "versionFilesRelativeToPath");

			if (versions == null)
				throw new ArgumentNullException("versions");

			XmlWriterSettings settings = new XmlWriterSettings();
			settings.Indent = true;

			using (XmlWriter writer = XmlWriter.Create(stream, settings))
			{
				writer.WriteStartDocument();
				writer.WriteStartElement("sources");

				IDictionary<string, IList<SourceVersion>> keyedVersions = GroupBy(false, 
					delegate(SourceVersion version) { return version.sourceKey; }, versions, StringComparer.Ordinal);

				foreach (KeyValuePair<string, IList<SourceVersion>> pair in keyedVersions)
				{
					writer.WriteStartElement("source");
					writer.WriteAttributeString("key", pair.Key);
					writer.WriteAttributeString("version", sourceVersionNames[pair.Key]);

					if (pair.Value != null)
					{
						foreach (SourceVersion version in pair.Value)
						{
							writer.WriteStartElement("version");
							writer.WriteAttributeString("path", version.path.CompleteRelativeTo(versionFilesRelativeToPath));
							writer.WriteAttributeString("version", version.version);
							writer.WriteEndElement();
						}
					}

					writer.WriteEndElement();
				}
				
				// write references that have no versioned sources specified
				foreach (KeyValuePair<string, string> pair in sourceVersionNames)
				{
					if (keyedVersions.ContainsKey(pair.Key))
					{
						IList<SourceVersion> sources = keyedVersions[pair.Key];

						if (sources != null && sources.Count > 0)
							continue;
					}

					writer.WriteStartElement("source");
					writer.WriteAttributeString("key", pair.Key);
					writer.WriteAttributeString("version", sourceVersionNames[pair.Key]);
					writer.WriteEndElement();
				}

				writer.WriteEndDocument();
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
		public static IDictionary<TKey, IList<SourceVersion>> GroupBy<TKey>(bool clone, Converter<SourceVersion, TKey> groupByKey, 
			IEnumerable<SourceVersion> versions, IEqualityComparer<TKey> comparer)
		{
			if (versions == null)
				throw new ArgumentNullException("versions");

			Dictionary<TKey, IList<SourceVersion>> keys = new Dictionary<TKey, IList<SourceVersion>>(comparer);

			foreach (SourceVersion version in versions)
			{
				if (version == null)
					continue;

				SourceVersion versionClone = (clone) ? new SourceVersion(version) : version;

				TKey key = groupByKey(versionClone);

				IList<SourceVersion> list;

				if (keys.ContainsKey(key))
					list = keys[key];
				else
					keys.Add(key, list = new List<SourceVersion>());

				if (!list.Contains(versionClone))
					list.Add(versionClone);
			}

			return keys;
		}

		public override string ToString()
		{
			return path.CompletePath;
		}
		#endregion
	}
}
